import path from "path";
import fs from "fs";
import assert from "assert";
import { globbySync } from "globby";
import is from "is-type-of";
import { defaultCamelize, loadFile } from "../utils/index.js";
import debugFactory from "debug";
const debug = debugFactory("core:loader/file_loader");
import { FULLPATH, EXPORTS } from "../constants.js";
/**
 * @class FileLoader 加载文件类
 * @param {Object} options 配置
 * @param {String|Array} options.directory 被加载的目录
 * @param {Object} options.target 加载文件挂载目标对象
 * @param {String} options.match 匹配加载的文件，支持glob，默认全部js
 * @param {String} options.ignore 忽略匹配加载的文件
 * @param {String|Function} options.caseStyle 将文件路径转换为属性时设置属性的大小写风格
 * @param {Function} options.initializer 导出文件包装方法
 * @param {Boolean} options.iscall 是否需要执行注入后导出，默认为true
 * @param {Object} options.inject 注入对象
 * @param {Object} options.override 是否覆盖
 * @param {Object} options.isflat 是否扁平化
 */
export default class FileLoader {
  constructor(options) {
    assert(options.directory, "必须设置options.directory");
    assert(options.target, "必须设置options.target");

    const DEFAULTS = {
      directory: null,
      target: null,
      match: undefined,
      ignore: undefined,
      caseStyle: "camel",
      iscall: true,
      initializer: null,
      inject: null,
      isoverride: false,
      isflat: false,
    };

    this.options = Object.assign({}, DEFAULTS, options);
  }

  /**
   * @function load 加载文件
   * @description
   * 将js文件附加到目标对象。将目录映射成属性。
   * 如：`app/controller/dir/index.js` 文件将转换成 `target.dir.index` 属性。
   * @return {Object} target
   */
  async load() {
    const _this = this;
    const { target, isflat } = _this.options;
    const items = await _this.parseFiles();
    for (const item of items) {
      if (isflat) {
        const obj = item.exports;
        if (obj && !is.primitive(obj)) {
          obj[FULLPATH] = item.fullpath;
          obj[EXPORTS] = true;
        }
        target[item.properties.join(".")] = obj;
      } else {
        debug("loading: %o", item);
        // item { properties: [ 'a', 'b', 'c'], exports } 转成成 target.a.b.c = exports
        item.properties.reduce((target, property, index) => {
          let obj;
          const properties = item.properties.slice(0, index + 1).join(".");
          if (index === item.properties.length - 1) {
            if (property in target) {
              if (!_this.options.isoverride) {
                throw new Error(
                  `不能覆盖属性 '${properties}' 属性 ${target[property][FULLPATH]} 来自 ${item.fullpath}`
                );
              }
            }
            obj = item.exports;
            if (obj && !is.primitive(obj)) {
              obj[FULLPATH] = item.fullpath;
              obj[EXPORTS] = true;
            }
          } else {
            obj = target[property] || {};
          }
          target[property] = obj;
          debug("loaded %s", properties);
          return obj;
        }, target);
      }
    }
    return target;
  }

  /**
   * @function parseFiles 解析文件
   * @description
   * 分析指定目录中的文件，然后返回文件列表，每个文件都包含属性和导出。
   * 如：`app/controller/dir/index.js`
   * ```javascript
   * import { Controller } from "@colpu/core";
   * export default class IndexController extends Controller {};
   * ```
   * 此文件将返回：
   * ```javascript
   * {
   *   properties: [ 'dir', 'index' ],
   *   exports: class IndexController extends Controller {},
   * }
   * ```
   * `properties` 是包含文件路径目录的数组.
   * `exports` 取决于类型，如果导出是一个函数，则将调用它。如果指定了初始值设定项，则将使用导出调用该初始值设定项以进行自定义。
   * @return {Primise}
   */
  async parseFiles() {
    const _this = this;
    let { match, ignore, directory } = _this.options;

    // 处理匹配文件模式
    if (!match) {
      match = ["**/*.{js,ts}"];
    } else {
      match = Array.isArray(match) ? match : [match];
    }

    // 处理忽略文件
    if (ignore) {
      ignore = Array.isArray(ignore) ? ignore : [ignore];
      ignore = ignore.filter((f) => !!f).map((f) => "!" + f);
      match = match.concat(ignore);
    }

    // 目录组装成数组
    if (!Array.isArray(directory)) {
      directory = [directory];
    }

    const items = [];
    debug("parse files: %j", directory);
    for (const dir of directory) {
      const filepaths = globbySync(match, {
        cwd: dir,
      });

      for (const filepath of filepaths) {
        const fullpath = path.join(dir, filepath);
        if (!fs.statSync(fullpath).isFile()) {
          continue;
        }

        // 获得属性，如： app/service/foo/bar.js => [ 'foo', 'bar' ]
        const properties = _this.getProperties(filepath, _this.options);
        const modulePath = `src${path.sep}modules`;
        if (dir.includes(modulePath)) {
          const moduleName = dir.split(modulePath)[1].split(path.sep)[1];
          const hasDir = items.filter(item => {
            const dirs = item.pathname.split('.');
            return dirs[1] === moduleName && !dirs[2];
          });
          if (hasDir.length > 0) {
            throw new Error(`不能使用src/modules/${moduleName}的目录结构`);
          }
          properties.unshift(moduleName);
        }

        // 转换属性为：app/service/foo/bar.js => service.foo.bar
        const pathname = `${dir.split(/[/\\]/).slice(-1)}.${properties.join(
          "."
        )}`;

        // 获得导出文件
        const exports = await _this.getExports(
          fullpath,
          _this.options,
          pathname
        );

        // 忽略文件为null
        if (exports === null) {
          continue;
        }

        // set properties of class
        if (is.class(exports)) {
          exports.prototype.pathname = pathname;
          exports.prototype.fullpath = fullpath;
        }

        items.push({
          pathname,
          fullpath,
          properties,
          exports,
        });
        debug(
          "fullpath: %s, properties: %j, exports: %o",
          fullpath,
          properties,
          exports
        );
      }
    }
    return items;
  }

  /**
   * @function getProperties
   * @description 将文件路径转换为属性数组
   * 如：a/b/c.js => ['a', 'b', 'c']
   * @return {Array}
   */
  getProperties(filepath, { caseStyle }) {
    // 如果caseStyle是function这使用自定义方式
    if (is.function(caseStyle)) {
      const result = caseStyle(filepath);
      assert(is.array(result), `caseStyle expect an array, but got ${result}`);
      return result;
    }

    // 默认使用驼峰化风格
    const properties = filepath
      .substring(0, filepath.lastIndexOf("."))
      .split("/");
    return properties.map((property) => {
      return defaultCamelize(property, caseStyle);
    });
  }

  /**
   * @function getExports 获取到导出的文件
   * @description
   * 从文件路径获取导出，如果导出为null/undefined，则将忽略它
   * @param {String} fullpath 导出文件路径
   * @param {Object} param
   * {
   * initializer: null, // 包装导出文件方法，默认为null
   * iscall: false, // 导出文件是否需要执行注入后导出，默认为false
   * inject: null, // 要注入的对象
   * }
   * @param {String} pathname
   */
  async getExports(fullpath, { initializer, iscall, inject }, pathname) {
    let exports = await loadFile(fullpath);

    // 根据需要初始化导出文件，即导出前进行一次包装
    if (initializer) {
      exports = initializer(exports, {
        fullpath,
        pathname,
      });
    }

    // 返回导出exports文件，当是class或generator
    // module.exports = class Service {};
    // or
    // module.exports = function*() {}
    if (
      is.class(exports) ||
      is.generatorFunction(exports) ||
      is.asyncFunction(exports)
    ) {
      debug("导出文件是class或generatorFunction或asyncFunction: %o", exports);
      return exports;
    }

    // 返回导出exports文件， 当存在inject，并且是function
    // module.exports = function(app) { return {}; }
    if (iscall && is.function(exports)) {
      if (!inject) {
        throw new Error(`当iscall为true时，必须设置inject参数`);
      }
      debug("导出文件是function，执行inject: %o", exports);
      let callExports = exports(inject);
      if (callExports != null) {
        debug("导出文件是执行inject: %o", callExports);
        return callExports;
      }
    }

    // 返回导出exports文件
    debug("导出文件: %o", exports);

    return exports;
  }
}
